﻿using PetaPoco.Core;
using PetaPoco.Providers;
using System;
using System.Collections.Generic;
using System.Linq;

namespace PetaPoco
{
    /// <summary>
    /// 内部批次方法提供者实现类
    /// </summary>
    internal class BatchProvider
    {
        /// <summary>
        /// 构造函数
        /// </summary>
        /// <param name="db">IDatabase 实例</param>
        public BatchProvider(IDatabase db)
        {
            Database = db;
        }

        /// <summary>
        /// 创建 BatchProvider 实例
        /// </summary>
        /// <param name="db">IDatabase 实例</param>
        /// <returns></returns>
        public static BatchProvider Create(IDatabase db)
        {
            BatchProvider provider = null;
            switch (db.Provider.GetType().Name)
            {
                case nameof(SqlServerDatabaseProvider):
                    provider = new SqlServerBatchProvider(db);
                    break;
                case nameof(SQLiteDatabaseProvider):
                    provider = new SQLiteBatchProvider(db);
                    break;
                default:
                    provider = new BatchProvider(db);
                    break;
            }
            return provider;
        }

        /// <summary>
        /// 获得 IDatabase 实例
        /// </summary>
        public IDatabase Database { get; }

        /// <summary>
        /// 获得/设置 每批实体大小，此值通过测试得出相对插入速度比较快的一个值 默认 50
        /// </summary>
        public int BatchSize { get; protected set; } = 50;

        /// <summary>
        /// 获得/设置 是否支持批量插入功能
        /// </summary>
        public bool SupportBulk { get; set; }

        /// <summary>
        /// 批次插入方法
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="pocos"></param>
        public virtual void InsertBatch<T>(IEnumerable<T> pocos)
        {
            if (SupportBulk)
            {
                InsertBulk(pocos);
            }
            else
            {
                var pd = PocoData.ForType(typeof(T), Database.DefaultMapper);
                InsertBatch(pd, pocos);
            }
        }

        /// <summary>
        /// 批次插入方法
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="primaryKeyName"></param>
        /// <param name="autoIncrement"></param>
        /// <param name="pocos"></param>
        public virtual void InsertBatch(string tableName, string primaryKeyName, bool? autoIncrement, IEnumerable<object> pocos)
        {
            if (SupportBulk)
            {
                InsertBulk(tableName, pocos);
            }
            else
            {
                var pd = PocoData.ForType(pocos.First().GetType(), Database.DefaultMapper);
                if (!string.IsNullOrEmpty(tableName)) pd.TableInfo.TableName = tableName;
                if (!string.IsNullOrEmpty(primaryKeyName)) pd.TableInfo.PrimaryKey = primaryKeyName;
                if (autoIncrement.HasValue) pd.TableInfo.AutoIncrement = autoIncrement.Value;
                InsertBatch(pd, pocos);
            }
        }

        /// <summary>
        /// Bulk批次插入方法具体实现
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="pocos"></param>
        protected virtual void InsertBulk<T>(IEnumerable<T> pocos) => throw new NotImplementedException();

        /// <summary>
        /// Bulk批次插入方法具体实现
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="pocos"></param>
        protected virtual void InsertBulk(string tableName, IEnumerable<object> pocos) => throw new NotImplementedException();

        /// <summary>
        /// 拼接字符串方法具体实现
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="pd"></param>
        /// <param name="pocos"></param>
        protected virtual void InsertBatch<T>(PocoData pd, IEnumerable<T> pocos)
        {
            try
            {
                /*
                 * 将 entities 分批插入数据库
                 * 每批生成 insert into TableName(...) values(...),(...)... 
                 * 该方法相对循环一条一条插入，速度提升 2/3 这样
                 */

                var primaryKeyName = pd.TableInfo.PrimaryKey;
                var autoIncrement = pd.TableInfo.AutoIncrement;
                var _provider = Database.Provider;
                var _paramPrefix = _provider.GetParameterPrefix(Database.ConnectionString);
                var args = new List<object>();
                var batchSql = new List<string>();

                Database.BeginTransaction();
                pocos.ToList().ForEach(poco =>
                {
                    var names = new List<string>();
                    var values = new List<string>();
                    foreach (var i in pd.Columns)
                    {
                        // Don't insert result columns
                        if (i.Value.ResultColumn)
                            continue;

                        // Don't insert the primary key (except under oracle where we need bring in the next sequence value)
                        if (autoIncrement && primaryKeyName != null && string.Compare(i.Key, primaryKeyName, true) == 0)
                        {
                            // Setup auto increment expression
                            var autoIncExpression = _provider.GetAutoIncrementExpression(pd.TableInfo);
                            if (autoIncExpression != null)
                            {
                                names.Add(i.Key);
                                values.Add(autoIncExpression);
                            }
                            continue;
                        }

                        names.Add(_provider.EscapeSqlIdentifier(i.Key));
                        values.Add(ConvertValue(i.Value.GetValue(poco), _paramPrefix, args));
                    }
                    if (batchSql.Count == 0)
                    {
                        batchSql.Add(GenerateHeaderSql(Database.Provider.EscapeTableName(pd.TableInfo.TableName), names.ToArray(), values.ToArray()));
                    }
                    else
                    {
                        batchSql.Add(GenerateBodySql(values.ToArray()));
                    }

                    if (batchSql.Count == BatchSize)
                    {
                        InsertIntoDb(batchSql, args);
                    }
                });

                // 剩余数据
                if (batchSql.Count > 0) InsertIntoDb(batchSql, args);

                Database.CompleteTransaction();
            }
            catch (Exception)
            {
                Database.AbortTransaction();
                throw;
            }
        }

        private void InsertIntoDb(List<string> sqls, List<object> args)
        {
            Database.Execute(string.Join("", sqls), args.ToArray());
            sqls.Clear();
            args.Clear();
        }

        protected virtual string GenerateHeaderSql(string tableName, string[] names, string[] values) => $"INSERT INTO {tableName} ({string.Join(",", names)}) VALUES ({string.Join(",", values)})";

        protected virtual string GenerateBodySql(string[] values) => $",({string.Join(",", values)})";
        /// <summary>
        /// 生成数据库插入语句
        /// </summary>
        /// <param name="tableName"></param>
        /// <param name="names"></param>
        /// <param name="values"></param>
        /// <returns></returns>
        protected virtual string GenerateSql(string tableName, string[] names, string[] values) => $"INSERT INTO {tableName} ({string.Join(",", names)}) VALUES ({string.Join(",", values)})";

        /// <summary>
        /// 数据类型转化方法
        /// </summary>
        /// <param name="val"></param>
        /// <param name="paramPrefix"></param>
        /// <param name="args"></param>
        /// <returns></returns>
        protected virtual string ConvertValue(object val, string paramPrefix, List<object> args)
        {
            if (val == null) return "NULL";
            var valType = val.GetType();
            if (valType.IsEnum)
            {
                val = Convert.ChangeType(val, Enum.GetUnderlyingType(valType));
                valType = val.GetType();
            }
            if (Utils.IsToStringableNumericType(valType)) return val.ToString();

            if (val is bool) return Database.Provider.MapParameterValue(val).ToString();
            var ret = $"{paramPrefix}{args.Count}";
            args.Add(val);
            return ret;
        }
    }
}
